using System;
using System.Collections;
using System.Collections.Generic;

using Rockfan.Framework;
using Rockfan.Framework.Audio.Midi;

using System.IO;
using System.Xml;
using System.Xml.Linq;

namespace Rockfan.Framework.Audio
{
	public sealed class Parser
	{
        public BeatTrack Beats { get; private set; }
		public TempoTrack TempoTrack { get; private set; }
		public TimeSignatureTrack TimeSignatureTrack { get; private set; }
        public List<NoteTrack> NoteTracks { get; private set; }
	
		private Parser()
		{
            TempoTrack = new TempoTrack("Tempo");
            TimeSignatureTrack = new TimeSignatureTrack("TimeSignature");
            NoteTracks = new List<NoteTrack>();
            Beats = new BeatTrack("Beats");
		}

		public static Parser Load(string fileLocation)
        {
            #region Get Midi File

            if (fileLocation.EndsWith(".mid") || fileLocation.EndsWith(".midi"))
            {
                var midi = MidiFormat.Load(fileLocation);
                var quaterNote = (int)(midi.PPQN / 2.0f);
                var parser = new Parser();

                var statusStart = 3360;
                var status = 3360;
                var beats = 0;
                var numerator = 4;
                var denominator = 4;
                var tempo = midi.PPQN * 1000.0f;
                var lastNoteTime = 0.0f;
                var extraMeasures = 0;

                #region Convert Tempo Track Events

                foreach (var tempoEvent in midi.TempoTrack.Events)
                {
                    var tEvent = (TempoEvent)tempoEvent;
                    var time = midi.GetSeconds(tEvent.Start);
                    parser.TempoTrack.AddEvent(new TempoTrackEvent(time, tEvent.BPM));
                }

                #endregion

                #region Convert Time Signature Events

                foreach (var timeSigEvent in midi.TimeSignatureTrack.Events)
                {
                    var tsEvent = (TimeSignatureEvent)timeSigEvent;
                    var time = midi.GetSeconds(tsEvent.Start);
                    parser.TimeSignatureTrack.AddEvent(new TimeSignatureTrackEvent(time, tsEvent.Numerator, tsEvent.Denominator));
                }

                #endregion

                #region Convert All Midi Tracks

                for (int i = 0; i < midi.Count; i++)
                {
                    var midiTrack = midi[i];
                    var noteList = new List<NoteEvent>();
                    var track = new NoteTrack(midiTrack.Name);
                    
                    parser.NoteTracks.Add(track);

                    foreach (var note in midiTrack.Events)
                    {
                        if (note is NoteEvent)
                        {
                            var startNote = (NoteEvent)note;

                            if (!startNote.IsNoteOn)
                            {
                                for (int index = noteList.Count - 1; index >= 0; index--)
                                {
                                    var endNote = noteList[index];

                                    if (startNote.NoteID == endNote.NoteID)
                                    {
                                        var id = startNote.NoteID;
                                        var startTime = midi.GetSeconds(endNote.Start);
                                        var endTime = midi.GetSeconds(startNote.Start);

                                        var difficulty = id >= 60 && id <= 71 ? Difficulty.Easy :
                                                         id >= 72 && id <= 83 ? Difficulty.Medium :
                                                         id >= 84 && id <= 95 ? Difficulty.Hard :
                                                         id >= 96 && id <= 107 ? Difficulty.Expert :
                                                         Difficulty.ExpertPlus;

                                        if (difficulty != Difficulty.ExpertPlus)
                                        {
                                            var idType = (NoteType)Math.Pow(2.0f, (int)((id - 60) % 12) + 1);
                                            track.AddEvent(new NoteTrackEvent(startTime, endTime - startTime, idType, difficulty));
                                        }

                                        noteList.Remove(endNote);
                                        continue;
                                    }
                                }
                            }
                            else noteList.Add(startNote);
                        }
                    }
                }

                #endregion

                #region Get the last note time for all tracks

                foreach (var track in parser.NoteTracks)
                {
                    if (track.Count <= 0)
                        continue;

                    track.SortAccending(); // I demand perfection

                    var index = track.Count - 1; // get the last note index on the track
                    var tEvent = track[index--];

                    #region Get the Last Note Time for this track

                    while (true)
                    {
                        if (tEvent == null || index < 0)
                            break;

                        if (tEvent.GetType() != typeof(NoteTrackEvent))
                        {
                            tEvent = track[index--];
                            continue;
                        }

                        var noteEvent = (NoteTrackEvent)tEvent;
                        var totalLength = noteEvent.Start + noteEvent.Length;

                        if (totalLength >= lastNoteTime)
                            lastNoteTime = totalLength;

                        break;
                    }

                    #endregion
                }

                #endregion
                
                #region Error Check

                // Something went horribly wrong, and this file has no midi note events
                if (lastNoteTime == 0)
                    throw new NullReferenceException("this file does not contain any notes");

                #endregion

                #region Setup the Beat Track

                if (midi.TempoTrack.Events.Count > 0)
                    tempo = (midi.TempoTrack.Events[midi.TempoTrack.Events.Count - 1] as TempoEvent).Tempo;

                var bpm = 6E+07f / (float)tempo;
                var beatOffset = midi.ConvertToSeconds(quaterNote, tempo);

                while (midi.GetSeconds(status) <= lastNoteTime)
                {
                    var id = MarkerType.Measure;

                    #region Find the correct tempo for the current status

                    foreach (var tEvent in midi.TempoTrack.Events)
                    {
                        if (tEvent.Start + statusStart == status)
                            bpm = (tEvent as TempoEvent).BPM;

                        else if (tEvent.Start + statusStart > status)
                            break;
                    }

                    #endregion

                    #region Find the correct Time Signature for the Current status

                    foreach (var tsEvent in midi.TimeSignatureTrack.Events)
                    {
                        if (tsEvent.Start + statusStart == status)
                        {
                            numerator = (tsEvent as TimeSignatureEvent).Numerator;
                            denominator = (tsEvent as TimeSignatureEvent).Denominator;
                        }
                        else if (tsEvent.Start + statusStart > status)
                            break;
                    }

                    #endregion                    

                    // Set the marker event as an Measure Event
                    if (beats % (numerator * denominator) == 0)
                        id = MarkerType.Measure; 

                    // Set the marker event as a Beat event
                    else if ((beats * numerator) % (numerator * denominator) == 0)
                        id = MarkerType.Beat;

                    // Set the marker event as a eigth note event
                    else id = MarkerType.Eigth;

                    var currentTime = midi.GetSeconds(status);
                    parser.Beats.AddEvent(new BeatTrackEvent(currentTime, id, bpm));

                    // Check to see if this is a BeatMeasure, if so, will the next quarter note, quit the loop,
                    // otherwise extend the lastNoteTime by a quarterNote,so we can try and end on a Measure.
                    if ((beats % (numerator * denominator) != 0) && midi.GetSeconds(status + quaterNote) > lastNoteTime)
                        lastNoteTime += beatOffset;

                    // Some weird hack to make sure we have at least 2 full measures of beats after the end of the song
                    else if (midi.GetSeconds(status + quaterNote) >= lastNoteTime)
                    {
                        if ((beats % (numerator * denominator) == 0) && extraMeasures++ > 1)
                            break;

                        lastNoteTime += beatOffset;
                    }

                    // increment the beats, and change the status
                    beats++;
                    status += quaterNote;                    
                }

                #endregion

                #region Stream Writer

                //using (var fStream = new FileStream(Path.GetDirectoryName(fileLocation) + @"\song.cache", FileMode.Create))
                //{
                //    var cache = new XElement("SongCache");

                //    for (int i = 0; i < midi.Count; i++)
                //    {
                //        var midiTrack = midi[i];
                //        var trackElement = new XElement("track", new XAttribute("name", midiTrack.Name), new XAttribute("ppqn", midiTrack.PPQN));

                //        foreach (var trackEvent in midiTrack.Events)
                //        {
                //            if (trackEvent is TempoEvent)
                //            {
                //                var tempoEvent = (TempoEvent)trackEvent;
                //                var tempoElement = new XElement("tempo", new XAttribute("start", tempoEvent.Start), new XAttribute("bpm", tempoEvent.BPM));
                //                trackElement.Add(tempoElement);

                //            }
                //            else if (trackEvent is Midi.NoteEvent)
                //            {
                //                var noteEvent = (NoteEvent)trackEvent;
                //                var noteElement = new XElement("note", 
                //                    new XAttribute("start", noteEvent.Start), 
                //                    new XAttribute("noteon", noteEvent.IsNoteOn), 
                //                    new XAttribute("id", noteEvent.NoteID), 
                //                    new XAttribute("pulses", noteEvent.Pulses));

                //                trackElement.Add(noteElement);

                //            }
                //            else if (trackEvent is Midi.TextEvent)
                //            {
                //                var textEvent = (TextEvent)trackEvent;

                //                var textElement = new XElement("text",
                //                    new XAttribute("start", textEvent.Start), 
                //                    new XAttribute("text", textEvent.Text));

                //                trackElement.Add(textElement);

                //            }
                //            else if (trackEvent is Midi.TimeSignatureEvent)
                //            {
                //                var signatureEvent = (TimeSignatureEvent)trackEvent;
                //                var signatureElement = new XElement("timesig",
                //                    new XAttribute("start", signatureEvent.Start),
                //                    new XAttribute("numerator", signatureEvent.Numerator), 
                //                    new XAttribute("denominator", signatureEvent.Denominator),
                //                    new XAttribute("clicks", signatureEvent.ClocksPerClick),
                //                    new XAttribute("npq", signatureEvent.ThirtySecondNotesPerQuarter));

                //                trackElement.Add(signatureElement);
                //            }
                //        }

                //        cache.Add(trackElement);
                //    }

                //    using (var sWriter = new StreamWriter(fStream))
                //        sWriter.Write(cache.ToString(SaveOptions.None));
                //}

                #endregion

                return parser;
            }

            #endregion

            return null;
        }

        private static int GetNoteCount(NoteType type)
        {
            var count = 0;

            if ((type & NoteType.Green) == NoteType.Green)
                count++;

            if ((type & NoteType.Red) == NoteType.Red)
                count++;

            if ((type & NoteType.Yellow) == NoteType.Yellow)
                count++;

            if ((type & NoteType.Blue) == NoteType.Blue)
                count++;

            if ((type & NoteType.Orange) == NoteType.Orange)
                count++;

            return count;
        }

        private static bool GetIsHopo(NoteType type)
        {
            return (type & NoteType.Hopo) == NoteType.Hopo;
        }
	}
}